{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Performance\n",
    "\n",
    "This notebook measures performance of `DiscreteEvents.jl` functionality in order to compile the [performance section](https://pbayer.github.io/DiscreteEvents.jl/dev/performance/) of the documentation."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "using DiscreteEvents, BenchmarkTools, Random\n",
    "res = Dict(); # results dictionary"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Event-based simulations"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The following is a modification of the [channel example](https://pbayer.github.io/DiscreteEvents.jl/dev/approach/#Event-based-modeling-1). We simulate events \n",
    "\n",
    "1. taking something from a common channel or waiting if there is nothing, \n",
    "2. then taking a delay, doing a calculation and\n",
    "3. returning three times to the first step.\n",
    "\n",
    "As calculation we take the following Machin-like sum:\n",
    "\n",
    "$$4 \\sum_{k=1}^{n} \\frac{(-1)^{k+1}}{2 k - 1}$$\n",
    "\n",
    "This gives a slow approximation to $\\pi$. The benchmark creates long queues of timed and conditional events and measures how fast they are handled."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Function calls as events\n",
    "\n",
    "The first implementation is based on events with `SimFunction`s. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "setup (generic function with 1 method)"
      ]
     },
     "execution_count": 2,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "function take(id::Int64, qpi::Vector{Float64}, step::Int64)\n",
    "    if isready(ch)\n",
    "        take!(ch)                                            # take something from common channel\n",
    "        event!(SF(put, id, qpi, step), after, rand())    # timed event after some time\n",
    "    else\n",
    "        event!(SF(take, id, qpi, step), SF(isready, ch)) # conditional event until channel is ready\n",
    "    end\n",
    "end\n",
    "\n",
    "function put(id::Int64, qpi::Vector{Float64}, step::Int64)\n",
    "    put!(ch, 1)\n",
    "    qpi[1] += (-1)^(id+1)/(2id -1)      # Machin-like series (slow approximation to pi)\n",
    "    step > 3 || take(id, qpi, step+1)\n",
    "end\n",
    "\n",
    "function setup(n::Int)                     # a setup he simulation\n",
    "    reset!(𝐶)\n",
    "    Random.seed!(123)\n",
    "    global ch = Channel{Int64}(32)  # create a channel\n",
    "    global qpi = [0.0]\n",
    "    si = shuffle(1:n)\n",
    "    for i in 1:n\n",
    "        take(si[i], qpi, 1)\n",
    "    end\n",
    "    for i in 1:min(n, 32)\n",
    "        put!(ch, 1) # put first tokens into channel 1\n",
    "    end\n",
    "end"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "If we setup 250 summation elements, we get 1000 timed events and over 1438 sample steps with conditional events."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "  0.000588 seconds (2.05 k allocations: 64.031 KiB)\n",
      "  0.182265 seconds (1.59 M allocations: 34.813 MiB, 4.82% gc time)\n",
      "run! finished with 1000 clock events, 1438 sample steps, simulation time: 500.0\n",
      "result=3.1375926695894556\n"
     ]
    }
   ],
   "source": [
    "@time setup(250)\n",
    "println(@time run!(𝐶, 500))\n",
    "println(\"result=\", qpi[1])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {
    "scrolled": true
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "BenchmarkTools.Trial: \n",
       "  memory estimate:  34.80 MiB\n",
       "  allocs estimate:  1586345\n",
       "  --------------\n",
       "  minimum time:     170.198 ms (0.00% GC)\n",
       "  median time:      175.226 ms (1.65% GC)\n",
       "  mean time:        175.521 ms (1.33% GC)\n",
       "  maximum time:     180.847 ms (1.46% GC)\n",
       "  --------------\n",
       "  samples:          50\n",
       "  evals/sample:     1"
      ]
     },
     "execution_count": 5,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "t = run(@benchmarkable run!(𝐶, 500) setup=setup(250) evals=1 seconds=15.0 samples=50)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "170.19763999999998"
      ]
     },
     "execution_count": 6,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "res[\"Event based with SimFunctions\"] = minimum(t).time * 1e-6 # ms "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Expressions as events\n",
    "\n",
    "The 2nd implementation does the same but with expressions, which are `eval`uated in global scope during runtime. This gives a one-time warning for beeing slow:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "put (generic function with 1 method)"
      ]
     },
     "execution_count": 7,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "function take(id::Int64, qpi::Vector{Float64}, step::Int64)\n",
    "    if isready(ch)\n",
    "        take!(ch)                                            # take something from common channel\n",
    "        event!(:(put($id, qpi, $step)), after, rand())   # timed event after some time\n",
    "    else\n",
    "        event!(:(take($id, qpi, $step)), :(isready(ch))) # conditional event until channel is ready\n",
    "    end\n",
    "end\n",
    "\n",
    "function put(id::Int64, qpi::Vector{Float64}, step::Int64)\n",
    "    put!(ch, 1)\n",
    "    qpi[1] += (-1)^(id+1)/(2id -1)      # Machin-like series (slow approximation to pi)\n",
    "    step > 3 || take(id, qpi, step+1)\n",
    "end"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "  0.119963 seconds (233.55 k allocations: 12.248 MiB, 9.18% gc time)\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "┌ Warning: Evaluating expressions is slow, use `SimFunction` instead\n",
      "└ @ DiscreteEvents /Users/paul/.julia/packages/DiscreteEvents/nLVtr/src/clock.jl:291\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      " 11.029089 seconds (6.73 M allocations: 384.549 MiB, 0.51% gc time)\n",
      "run! finished with 1000 clock events, 1438 sample steps, simulation time: 500.0\n",
      "result=3.1375926695894556\n"
     ]
    }
   ],
   "source": [
    "@time setup(250)\n",
    "println(@time run!(𝐶, 500))\n",
    "println(\"result=\", sum(qpi))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "BenchmarkTools.Trial: \n",
       "  memory estimate:  382.13 MiB\n",
       "  allocs estimate:  6681246\n",
       "  --------------\n",
       "  minimum time:     10.963 s (0.40% GC)\n",
       "  median time:      10.976 s (0.38% GC)\n",
       "  mean time:        10.976 s (0.38% GC)\n",
       "  maximum time:     10.989 s (0.36% GC)\n",
       "  --------------\n",
       "  samples:          2\n",
       "  evals/sample:     1"
      ]
     },
     "execution_count": 9,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "t = run(@benchmarkable run!(𝐶, 500) setup=setup(250) evals=1 seconds=15.0 samples=50)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "Dict{Any,Any} with 2 entries:\n",
       "  \"Event based with Expressions\"  => 10962.8\n",
       "  \"Event based with SimFunctions\" => 170.198"
      ]
     },
     "execution_count": 11,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "res[\"Event based with Expressions\"] = minimum(t).time * 1e-6 #\n",
    "res"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "64.41237809172912"
      ]
     },
     "execution_count": 12,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "res[\"Event based with Expressions\"]/res[\"Event based with SimFunctions\"]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This takes much longer and shows that `eval` for Julia expressions, done in global scope is very expensive and should be avoided if performance is any issue."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Involving a global variable\n",
    "\n",
    "The third implementation works with `Simfunction`s like the first but involves a global variable `A`:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "setup (generic function with 1 method)"
      ]
     },
     "execution_count": 13,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "function take(id::Int64, qpi::Vector{Float64}, step::Int64)\n",
    "    if isready(ch)\n",
    "        take!(ch)                                       # take something from common channel\n",
    "        event!(SF(put, id, qpi, step), after, rand())    # timed event after some time\n",
    "    else\n",
    "        event!(SF(take, id, qpi, step), SF(isready, ch)) # conditional event until channel is ready\n",
    "    end\n",
    "end\n",
    "\n",
    "function put(id::Int64, qpi::Vector{Float64}, step::Int64)\n",
    "    put!(ch, 1)\n",
    "    global A += (-1)^(id+1)/(2id -1)      # Machin-like series (slow approximation to pi)\n",
    "    step > 3 || take(id, qpi, step+1)\n",
    "end\n",
    "\n",
    "function setup(n::Int)                     # a setup he simulation\n",
    "    reset!(𝐶)\n",
    "    Random.seed!(123)\n",
    "    global ch = Channel{Int64}(32)  # create a channel\n",
    "    global A = 0\n",
    "    si = shuffle(1:n)\n",
    "    for i in 1:n\n",
    "        take(si[i], qpi, 1)\n",
    "    end\n",
    "    for i in 1:min(n, 32)\n",
    "        put!(ch, 1) # put first tokens into channel 1\n",
    "    end\n",
    "end"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Variables\n",
      "  #self#\u001b[36m::Core.Compiler.Const(put, false)\u001b[39m\n",
      "  id\u001b[36m::Int64\u001b[39m\n",
      "  qpi\u001b[36m::Array{Float64,1}\u001b[39m\n",
      "  step\u001b[36m::Int64\u001b[39m\n",
      "\n",
      "Body\u001b[91m\u001b[1m::Any\u001b[22m\u001b[39m\n",
      "\u001b[90m1 ─\u001b[39m       Main.put!(Main.ch, 1)\n",
      "\u001b[90m│  \u001b[39m       nothing\n",
      "\u001b[90m│  \u001b[39m %3  = (id + 1)\u001b[36m::Int64\u001b[39m\n",
      "\u001b[90m│  \u001b[39m %4  = ((-1) ^ %3)\u001b[36m::Int64\u001b[39m\n",
      "\u001b[90m│  \u001b[39m %5  = (2 * id)\u001b[36m::Int64\u001b[39m\n",
      "\u001b[90m│  \u001b[39m %6  = (%5 - 1)\u001b[36m::Int64\u001b[39m\n",
      "\u001b[90m│  \u001b[39m %7  = (%4 / %6)\u001b[36m::Float64\u001b[39m\n",
      "\u001b[90m│  \u001b[39m %8  = (Main.A + %7)\u001b[91m\u001b[1m::Any\u001b[22m\u001b[39m\n",
      "\u001b[90m│  \u001b[39m       (Main.A = %8)\n",
      "\u001b[90m│  \u001b[39m %10 = (step > 3)\u001b[36m::Bool\u001b[39m\n",
      "\u001b[90m└──\u001b[39m       goto #3 if not %10\n",
      "\u001b[90m2 ─\u001b[39m       return %10\n",
      "\u001b[90m3 ─\u001b[39m %13 = (step + 1)\u001b[36m::Int64\u001b[39m\n",
      "\u001b[90m│  \u001b[39m %14 = Main.take(id, qpi, %13)\u001b[91m\u001b[1m::Any\u001b[22m\u001b[39m\n",
      "\u001b[90m└──\u001b[39m       return %14\n"
     ]
    }
   ],
   "source": [
    "ch = Channel{Int64}(32)\n",
    "@code_warntype put(1, qpi, 1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "  0.039028 seconds (89.01 k allocations: 4.505 MiB)\n",
      "  0.188030 seconds (1.59 M allocations: 35.104 MiB, 4.06% gc time)\n",
      "run! finished with 1000 clock events, 1438 sample steps, simulation time: 500.0\n",
      "result=3.1375926695894556\n"
     ]
    }
   ],
   "source": [
    "@time setup(250)\n",
    "println(@time run!(𝐶, 500))\n",
    "println(\"result=\", A)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "BenchmarkTools.Trial: \n",
       "  memory estimate:  34.83 MiB\n",
       "  allocs estimate:  1588345\n",
       "  --------------\n",
       "  minimum time:     172.230 ms (0.00% GC)\n",
       "  median time:      176.953 ms (1.79% GC)\n",
       "  mean time:        176.710 ms (1.42% GC)\n",
       "  maximum time:     181.112 ms (1.74% GC)\n",
       "  --------------\n",
       "  samples:          30\n",
       "  evals/sample:     1"
      ]
     },
     "execution_count": 16,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "t = run(@benchmarkable run!(𝐶, 500) setup=setup(250) evals=1 seconds=10.0 samples=30)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "Dict{Any,Any} with 3 entries:\n",
       "  \"Event based with Expressions\"                     => 10962.8\n",
       "  \"Event based with SimFunctions\"                    => 170.198\n",
       "  \"Event based with functions and a global variable\" => 172.23"
      ]
     },
     "execution_count": 17,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "res[\"Event based with functions and a global variable\"] = minimum(t).time * 1e-6 #\n",
    "res"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In this case the compiler does well to infer the type of `A` and it runs only marginally slower than the first version."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "@webio": {
   "lastCommId": null,
   "lastKernelId": null
  },
  "kernelspec": {
   "display_name": "Julia 1.3.0",
   "language": "julia",
   "name": "julia-1.3"
  },
  "language_info": {
   "file_extension": ".jl",
   "mimetype": "application/julia",
   "name": "julia",
   "version": "1.3.0"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
